import socket
import threading
import json
import os
import time
from tkinter import Tk, Text, END, filedialog, messagebox, ttk, Menu, Toplevel, BooleanVar, StringVar
import hashlib
import winsound
import re
import subprocess
import sys
import tkinter as tk

# 启动时自动创建必要目录
for d in [os.path.join("config"), os.path.join("config", "received_files"), os.path.join("config", "chat_logs")]:
    if not os.path.exists(d):
        os.makedirs(d)


class LoginWindow:
    def __init__(self):
        self.root = Tk()
        self.root.title("用户登录")
        self.root.resizable(False, False)
        self.nickname = None
        self.settings = self._load_settings()
        self._setup_ui()

        # 窗口居中
        self.root.update_idletasks()  # 确保获取到正确的窗口尺寸
        width = self.root.winfo_width()
        height = self.root.winfo_height()
        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()
        x = (screen_width - width) // 2
        y = (screen_height - height) // 2
        self.root.geometry(f'+{x}+{y}')  # 设置窗口位置

        self.root.mainloop()

    def _setup_ui(self):
        """构建登录界面"""
        main_frame = ttk.Frame(self.root, padding=20)
        main_frame.pack(expand=True)

        # 客户端IP显示
        ttk.Label(main_frame, text="客户端IP:").grid(row=0, column=0, sticky="w")
        self.ip_entry = ttk.Entry(main_frame, state="readonly")
        self.ip_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        self.ip_entry.config(state="normal")
        self.ip_entry.insert(0, self._get_client_ip())
        self.ip_entry.config(state="readonly")

        # 用户名输入
        ttk.Label(main_frame, text="用户名:").grid(row=1, column=0, sticky="w")
        self.user_entry = ttk.Entry(main_frame)
        self.user_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
        self.user_entry.insert(0, self.settings.get("nickname", ""))
        self.user_entry.bind("<Return>", self._on_login)

        # 按钮区域
        btn_frame = ttk.Frame(main_frame)
        btn_frame.grid(row=2, column=0, columnspan=2, pady=10)
        ttk.Button(btn_frame, text="登录", command=self._on_login).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="退出", command=self.root.destroy).pack(side="right", padx=5)

        # 默认焦点
        self.user_entry.focus_set()

    def _load_settings(self) -> dict:
        """加载本地配置文件"""
        try:
            with open(os.path.join("config", "client_settings.json"), "r", encoding="utf-8") as f:
                settings = json.load(f)
                settings.setdefault("nickname", "")
                settings.setdefault("server_ip", "")
                settings.setdefault("server_port", 12345)
                settings.setdefault("file_port", 12346)
                return settings
        except (FileNotFoundError, json.JSONDecodeError):
            return {"server_ip": "127.0.0.1", "server_port": 12345, "file_port": 12346, "nickname": ""}

    def _save_settings(self):
        """保存配置到文件"""
        try:
            with open(os.path.join("config", "client_settings.json"), "w", encoding="utf-8") as f:
                json.dump(self.settings, f, indent=2)
        except Exception as e:
            messagebox.showerror("保存失败", f"无法写入配置文件：{str(e)}")

    def _get_client_ip(self):
        """获取本机IP地址"""
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            return s.getsockname()[0]
        except Exception:
            return "127.0.0.1"

    def _on_login(self, event=None):
        """处理登录操作"""
        nickname = self.user_entry.get().strip()
        if not nickname:
            messagebox.showerror("输入错误", "用户名不能为空")
            return

        # 保存用户名到配置
        self.settings["nickname"] = nickname
        self._save_settings()

        self.nickname = nickname
        self.root.destroy()


class ChatClient:
    def __init__(self, nickname):
        # heartbeat
        self.heartbeat_interval = 30  # 心跳间隔时间（秒）
        self.last_heartbeat = time.time()
        # 加载用户设置
        self.settings = self._load_settings()
        self.nickname = nickname
        self.transfer_in_progress = False
        # 检查配置项
        if not self.settings.get("server_ip") or not self.settings.get("server_port"):
            messagebox.showerror("配置错误", "服务器IP或端口未设置，请检查config/client_settings.json")
            exit(1)

        # 初始化主窗口
        self.window = Tk()
        self.window.title(f"ChatOnLan [{nickname}]")
        self.window.geometry("800x600")
        self.window.resizable(False, False)
        self.window.bind('<Configure>', self._handle_window_move)
        self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
        self._setup_ui()

        # 连接服务器
        self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
        self.client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096)

        # 在连接成功后启动心跳线程
        try:
            self.client_socket.connect((self.settings["server_ip"], int(self.settings["server_port"])))
            self.client_socket.send(self.nickname.encode("utf-8"))
            threading.Thread(target=self.receive_messages, daemon=True).start()
            threading.Thread(target=self.send_heartbeat, daemon=True).start()  # 新增心跳线程
        except Exception as e:
            messagebox.showerror("连接失败", f"无法连接到服务器\n错误信息: {str(e)}")
            self.window.destroy()

    def _handle_window_move(self, event):
        """当主窗口移动时更新所有子窗口位置"""
        for child in self.window.winfo_children():
            if isinstance(child, Toplevel):
                self._center_window(child)

    def _center_window(self, child):
        """使子窗口居中于父窗口"""
        self.window.update_idletasks()  # 确保主窗口尺寸正确
        parent_x = self.window.winfo_x()
        parent_y = self.window.winfo_y()
        parent_width = self.window.winfo_width()
        parent_height = self.window.winfo_height()

        child.update_idletasks()  # 确保子窗口尺寸正确
        child_width = child.winfo_width()
        child_height = child.winfo_height()

        # 计算居中坐标
        x = parent_x + (parent_width - child_width) // 2
        y = parent_y + (parent_height - child_height) // 2
        child.geometry(f"+{x}+{y}")

    def _open_server_settings(self):
        def update_settings():
            ip = self.server_ip_var.get().strip()
            port = self.server_port_var.get().strip()

            # 端口校验
            try:
                port = int(port)
                if not (1 <= port <= 65535):
                    raise ValueError("端口号必须在 1 到 65535 之间")
            except ValueError as e:
                messagebox.showerror("输入错误", str(e))
                return

            self.settings["server_ip"] = ip
            self.settings["server_port"] = port
            self._save_settings()
            conf_win.destroy()
            messagebox.showinfo("成功", "服务器设置已更新")

        conf_win = Toplevel(self.window)
        conf_win.title("服务器设置")
        conf_win.resizable(False, False)
        self.server_ip_var = StringVar(value=self.settings.get("server_ip", ""))
        self.server_port_var = StringVar(value=self.settings.get("server_port", ""))

        ttk.Label(conf_win, text="IP：").grid(row=0, column=0, sticky="s")
        ttk.Entry(conf_win, textvariable=self.server_ip_var).grid(row=0, column=1, sticky="w")
        ttk.Label(conf_win, text="Port：").grid(row=1, column=0, sticky="s")
        ttk.Entry(conf_win, textvariable=self.server_port_var).grid(row=1, column=1, sticky="w")
        ttk.Button(conf_win, text="保存", command=update_settings).grid(row=2, column=0, sticky="e")
        ttk.Button(conf_win, text="取消", command=conf_win.destroy).grid(row=2, column=1, sticky="e")

        self._center_window(conf_win)
        conf_win.transient(self.window)
        conf_win.grab_set()

    # def _open_theme_settings(self):
    #     # 示例主题设置弹窗
    #     theme_win = Toplevel(self.window)
    #     theme_win.title("主题设置")
    #     ttk.Label(theme_win, text="开发中，敬请期待...").pack(padx=30, pady=20)
    #     self._center_window(theme_win)
    #     theme_win.transient(self.window)
    #     theme_win.grab_set()
    #
    def _open_sound_settings(self):
        sound_win = Toplevel(self.window)
        sound_win.title("声音设置")
        sound_win.resizable(False, False)
        self.sound_var = BooleanVar(value=self.settings.get("enable_sound", True))
        ttk.Checkbutton(sound_win, text="启用消息提示音", variable=self.sound_var,
                        command=lambda: self._toggle_sound(self.sound_var.get())).pack(padx=20, pady=15)
        self._center_window(sound_win)
        sound_win.transient(self.window)
        sound_win.grab_set()

    def _toggle_sound(self, enable):
        self.settings["enable_sound"] = enable

    def _save_settings(self):
        """保存设置到文件"""
        try:
            with open(os.path.join("config", "client_settings.json"), "w", encoding="utf-8") as f:
                json.dump(self.settings, f, indent=2)

                # 提示用户是否重新连接
                if messagebox.askyesno("设置已更新", "是否立即重新连接服务器？"):
                    self._reconnect_to_server()

        except Exception as e:
            messagebox.showerror("保存失败", f"无法写入配置文件：{str(e)}")

    def _save_chat_log(self, sender, content, msg_type="message", file_info=None):
        """保存聊天记录到本地文件"""
        log_dir = os.path.join("config", "chat_logs")
        try:
            # 检查并创建日志目录
            if not os.path.exists(log_dir):
                os.makedirs(log_dir)

            # 获取当前日志文件
            current_date = time.strftime("%Y%m%d")
            log_file = os.path.join(log_dir, f"{current_date}.log")

            # 检查文件大小，如果超过10MB则创建新文件
            if os.path.exists(log_file) and os.path.getsize(log_file) > 10 * 1024 * 1024:
                timestamp = time.strftime("%H%M%S")
                log_file = os.path.join(log_dir, f"{current_date}_{timestamp}.log")

            # 构建日志条目
            log_entry = {
                "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
                "sender": sender,
                "type": msg_type,
                "content": content,
                "is_error": False
            }

            if msg_type == "file" and file_info:
                log_entry.update({
                    "file_name": file_info.get("name"),
                    "file_size": file_info.get("size"),
                    "saved_path": file_info.get("saved_path")
                })

            # 写入日志文件
            with open(log_file, "a", encoding="utf-8") as f:
                f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")

        except Exception as e:
            print(f"保存日志失败: {str(e)}")

    def _load_chat_history(self, limit=30):
        """加载本地聊天记录"""
        history = []
        log_dir = os.path.join("config", "chat_logs")

        try:
            # 检查日志目录是否存在
            if not os.path.exists(log_dir):
                os.makedirs(log_dir)
                return []

            # 获取所有日志文件并按日期倒序排序
            log_files = sorted(
                [f for f in os.listdir(log_dir) if f.endswith(".log")],
                key=lambda x: x.split(".")[0],
                reverse=True
            )

            # 逆序读取文件内容
            for log_file in log_files:
                file_path = os.path.join(log_dir, log_file)
                try:
                    with open(file_path, "r", encoding="utf-8") as f:
                        lines = f.readlines()
                        # 从后向前读取，以保持时间顺序
                        for line in reversed(lines):
                            try:
                                record = json.loads(line.strip())
                                # 过滤有效消息记录
                                if record.get("type") == "message" and not record.get("is_error", False):
                                    history.append(record)
                                    if len(history) >= limit:
                                        return history  # 直接返回，不需要再次反转
                            except json.JSONDecodeError:
                                continue
                except Exception as e:
                    print(f"读取日志文件 {log_file} 时出错: {str(e)}")
                    continue

                if len(history) >= limit:
                    break

        except Exception as e:
            print(f"加载历史记录时出错: {str(e)}")

        return history

    def _display_history(self):
        """显示历史聊天记录"""
        try:
            self.chat_box.config(state="normal")
            self.chat_box.delete(1.0, END)
            self.chat_box.config(state="disabled")
            records = self._load_chat_history()
            if not records:
                return
            for record in records:
                is_self = (record["sender"] == self.nickname)
                self._display_history_message(
                    record["sender"],
                    record["content"],
                    record["timestamp"],
                    is_self
                )
        except Exception as e:
            print(f"显示历史记录时出错: {str(e)}")

    def _display_history_message(self, sender, content, timestamp, is_self=False):
        """显示历史消息（区分左右对齐）"""
        try:
            self.chat_box.config(state="normal")
            # 添加消息内容（只在消息之间添加一个换行）
            message = f"{content}\n\n"  # 移除多余的换行符
            self.chat_box.insert("1.0", message, "history_msg_self" if is_self else "history_msg_others")

            # 添加时间戳
            self.chat_box.insert("1.0", f"{timestamp}\n", "history_time")

            # 添加发送者（灰色显示历史标识）
            self.chat_box.insert("1.0", f"{sender}  ", "history_user")

            self.chat_box.see("end")
            self.chat_box.config(state="disabled")
        except Exception as e:
            print(f"显示历史消息时出错: {str(e)}")

    def _reconnect_to_server(self):
        """断开当前连接并尝试使用新设置重连"""
        try:
            self.client_socket.close()
        except:
            pass

        try:
            self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.client_socket.connect((self.settings["server_ip"], int(self.settings["server_port"])))
            self.client_socket.send(self.nickname.encode("utf-8"))
            threading.Thread(target=self.receive_messages, daemon=True).start()
        except Exception as e:
            messagebox.showerror("连接失败", f"无法连接到新服务器地址：{str(e)}")

    def _setup_ui(self):
        menubar = Menu(self.window)
        self.window.config(menu=menubar)

        # 设置菜单
        settings_menu = Menu(menubar, tearoff=0)
        menubar.add_cascade(label="设置", menu=settings_menu)
        # settings_menu.add_command(label="主题设置", command=self._open_theme_settings)
        settings_menu.add_command(label="通知声音", command=self._open_sound_settings)
        settings_menu.add_separator()
        settings_menu.add_command(label="服务器设置", command=self._open_server_settings)
        """构建用户界面"""
        # 字体配置
        self.time_font = ("Arial", 9)
        self.user_font = ("Times New Roman", 10)
        self.msg_font = ("Segoe UI", 10)

        # 消息显示区域
        self.chat_box = Text(
            self.window,
            wrap="word",
            state="disabled",
            bg="#FFFFFF",
            padx=15,
            pady=15,
            font=self.msg_font
        )
        self.chat_box.pack(expand=True, fill="both", padx=10, pady=5)

        # 输入区域
        input_frame = ttk.Frame(self.window)
        input_frame.pack(padx=10, pady=10, fill="x")

        # 文件发送按钮
        self.btn_file = ttk.Button(
            input_frame,
            text="📁",
            width=3,
            command=self.send_file
        )
        self.btn_file.pack(side="left", padx=(0, 5))

        # 消息输入框
        self.entry = ttk.Entry(input_frame, font=self.msg_font)
        self.entry.pack(side="left", expand=True, fill="x")
        self.entry.bind("<Return>", self.send_text_message)

        # 发送按钮
        self.btn_send = ttk.Button(
            input_frame,
            text="发送",
            command=self.send_text_message
        )
        self.btn_send.pack(side="left", padx=(5, 0))

        # 配置消息标签样式
        self._configure_tags()

        # 加载聊天记录
        self._display_history()

    def _configure_tags(self):
        """配置文本样式标签"""
        # 自己发送的消息
        self.chat_box.tag_configure("time_self", foreground="#666666", font=self.time_font, justify="right")
        self.chat_box.tag_configure("user_self", foreground="#009933", font=self.user_font, justify="right")
        self.chat_box.tag_configure("msg_self", foreground="#000000", font=self.msg_font, justify="right", spacing2=5)

        # 他人发送的消息
        self.chat_box.tag_configure("time_others", foreground="#666666", font=self.time_font, justify="left")
        self.chat_box.tag_configure("user_others", foreground="#0066CC", font=self.user_font, justify="left")
        self.chat_box.tag_configure("msg_others", foreground="#000000", font=self.msg_font, justify="left", spacing2=5)

        # 进度条样式
        self.chat_box.tag_configure("progress",
                                    foreground="#666666",
                                    font=("Microsoft YaHei", 10),
                                    spacing1=5,  # 上方间距
                                    spacing2=5,  # 行间距
                                    spacing3=5,  # 下方间距
                                    lmargin1=20,  # 左侧缩进
                                    lmargin2=20  # 第二行缩进
                                    )

        # 修改历史消息样式
        self.chat_box.tag_configure("history_time",
                                    foreground="#999999",
                                    font=("Arial", 8),
                                    spacing1=5,  # 上方间距
                                    spacing2=2)  # 行间距
        self.chat_box.tag_configure("history_user",
                                    foreground="#666666",
                                    font=("Times New Roman", 9),
                                    spacing2=2)  # 行间距
        self.chat_box.tag_configure("history_msg_self",
                                    foreground="#666666",
                                    font=("Segoe UI", 9),
                                    spacing2=5,  # 行间距
                                    lmargin1=20,  # 左侧缩进
                                    rmargin=20)  # 右侧缩进
        self.chat_box.tag_configure("history_msg_others",
                                    foreground="#666666",
                                    font=("Segoe UI", 9),
                                    spacing2=5,  # 行间距
                                    lmargin1=20,  # 左侧缩进
                                    rmargin=20)  # 右侧缩进

    def _display_message(self, sender, content, is_self=False, is_error=False):
        """显示消息"""
        self.chat_box.config(state="normal")

        # 添加发送者
        self.chat_box.insert(END, f"{sender}  ", "user_self" if is_self else "user_others")

        # 添加时间戳
        timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
        self.chat_box.insert(END, f"{timestamp}\n", "time_self" if is_self else "time_others")

        # 添加消息内容
        self.chat_box.insert(END, f"{content}\n\n", "msg_self" if is_self else "msg_others")

        # 滚动到最新消息
        self.chat_box.see(END)
        self.chat_box.config(state="disabled")

        # 保存日志（排除错误消息）
        if not is_error:
            self._save_chat_log(sender, content)

    def send_text_message(self, event=None):
        """发送文本消息（修复消息格式）"""
        message = self.entry.get().strip()
        if message:
            try:
                # 使用JSON格式封装消息
                msg_data = json.dumps({
                    "type": "message",
                    "sender": self.nickname,
                    "content": message,
                    "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
                })
                self.client_socket.send(msg_data.encode("utf-8"))
                self._display_message(self.nickname, message, is_self=True)
                self.entry.delete(0, END)
            except Exception as e:
                messagebox.showerror("发送失败", f"消息发送失败: {str(e)}")

    def send_heartbeat(self):
        """定时发送心跳包"""
        while not self.transfer_in_progress:
            try:
                # 发送心跳请求并等待响应
                if time.time() - self.last_heartbeat > self.heartbeat_interval:
                    self.client_socket.send(b"HEARTBEAT_REQUEST")
                    self.last_heartbeat = time.time()
                time.sleep(5)
            except:
                break

    def send_file(self):
        """发送文件"""
        file_path = filedialog.askopenfilename()
        if not file_path:
            return

        self.transfer_in_progress = True
        # 在新线程中执行文件发送操作
        threading.Thread(target=self._send_file_thread, args=(file_path,), daemon=True).start()

    def _send_file_thread(self, file_path):
        try:
            file_name = os.path.basename(file_path)
            # 文件名合法性校验
            if re.search(r"[\\/]|\.\.", file_name):
                messagebox.showerror("非法文件名", "文件名不能包含..、/、\\字符")
                return
            file_size = os.path.getsize(file_path)
            if file_size > 1024 * 1024 * 100:
                messagebox.showinfo("文件大小超出限制", "文件大小不能超过100MB")
                return
            md5_hash = hashlib.md5()
            with open(file_path, "rb") as f:
                while chunk := f.read(4096):
                    md5_hash.update(chunk)
            md5_hex = md5_hash.hexdigest()
            file_sender = self.nickname
            file_port = int(self.settings.get("file_port", 12346))
            file_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            file_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096)
            file_socket.connect((self.settings["server_ip"], file_port))
            # 记录本地上传socket的ip:port
            local_ip, local_port = file_socket.getsockname()
            self.last_upload_addr = f"{local_ip}:{local_port}"
            header = f"FILE_INIT:{file_name}:{file_size}:{md5_hex}:{file_sender}\n".encode('utf-8')
            file_socket.sendall(header)

            # ====== 上传进度条窗口（精简版） ======
            progress_win = Toplevel(self.window)
            progress_win.title(f"上传任务: {file_name}")
            progress_win.geometry("400x120")
            progress_win.resizable(False, False)
            progress_win.transient(self.window)
            progress_win.grab_set()
            progress_win.attributes('-topmost', True)
            # 居中
            self.window.update_idletasks()
            x = self.window.winfo_x() + (self.window.winfo_width() - 400) // 2
            y = self.window.winfo_y() + (self.window.winfo_height() - 120) // 2
            progress_win.geometry(f"400x120+{x}+{y}")
            ttk.Label(progress_win, text=f"{file_name}").pack(pady=10)
            progress_var = tk.DoubleVar(value=0)
            progress_bar = ttk.Progressbar(progress_win, maximum=100, length=350, variable=progress_var)
            progress_bar.pack(pady=5)
            progress_label = ttk.Label(progress_win, text="0.0% (0KB/{:.1f}KB)".format(file_size / 1024))
            progress_label.pack(pady=5)

            # ====== 上传循环，实时更新进度 ======
            sent = 0
            with open(file_path, "rb") as f:
                while True:
                    chunk = f.read(4096)
                    if not chunk:
                        break
                    file_socket.sendall(chunk)
                    sent += len(chunk)
                    percent = sent / file_size * 100
                    progress_var.set(percent)
                    progress_label.config(
                        text="{:.1f}% ({:.1f}KB/{:.1f}KB)".format(percent, sent / 1024, file_size / 1024))
                    progress_win.update()
            file_socket.close()
            # 上传完成后自动关闭进度窗口
            progress_win.destroy()

            self.window.after(0, lambda: self._display_message(self.nickname, f"已发送文件: {file_name}", is_self=True))
            file_info = {
                "name": file_name,
                "size": file_size,
                "saved_path": file_path
            }
            self._save_chat_log(self.nickname, "发送文件", msg_type="file", file_info=file_info)
        except Exception as e:
            messagebox.showerror("文件发送失败", str(e))
            try:
                progress_win.destroy()
            except:
                pass

    def receive_messages(self):
        while True:
            try:
                data = self.client_socket.recv(4096)
                if not data:
                    break

                # 处理心跳响应
                if data == b"HEARTBEAT_RESPONSE":
                    self.last_heartbeat = time.time()
                    continue

                # 处理普通消息
                try:
                    message = json.loads(data.decode('utf-8'))
                    if message["type"] == "message":
                        self._display_message(
                            message["sender"],
                            message["content"],
                            is_self=(message["sender"] == self.nickname)
                        )
                        self.playsound()
                    elif message["type"] == "file":
                        # 显示文件消息
                        file_msg = f"{message['sender']} 发送了文件: {message['file_name']} ({message['file_size'] / 1024:.1f}KB)"
                        self._display_message("系统", file_msg)
                        self.playsound()
                    elif message["type"] == "file_notice":
                        # 新文件上传通知
                        file_msg = (
                            f"新文件上传: {message['file_name']}\n"
                            f"size: {message['file_size'] / 1024:.1f}KB\n"
                            f"MD5: {message['md5']}\n"
                            f"来自: {message.get('sender', '未知')}\n"
                            f"ip: {message.get('ip', '未知')}"
                        )
                        self._display_message("系统", file_msg)
                        self.playsound()
                        sender = message.get('sender', '')
                        # 仅非自己上传的文件才询问是否下载（用本地昵称判断）
                        if not hasattr(self, 'last_upload_addr') or sender != self.nickname:
                            file_info = {
                                "file_name": message['file_name'],
                                "file_size": message['file_size'],
                                "md5": message['md5'],
                                "sender": sender,
                                "ip": message['ip']
                            }
                            self.window.after(0, self.ask_download_file, file_info)
                except json.JSONDecodeError:
                    self._display_message("系统", "收到无效消息格式", is_error=True)
                except Exception as e:
                    self._display_message("系统", f"消息处理错误: {str(e)}", is_error=True)

            except ConnectionAbortedError:
                self._display_message("系统", "与服务器的连接已断开", is_error=True)
                break
            except Exception as e:
                self._display_message("系统", f"连接异常: {str(e)}", is_error=True)
                break

    def ask_download_file(self, file_info):
        if messagebox.askyesno("系统", f"收到新文件：{file_info['file_name']}，是否下载该文件？"):
            self.download_file_from_server(file_info['file_name'])

    def _save_file(self, file_info):
        """保存接收的文件"""
        save_dir = os.path.join("config", "received_files")
        os.makedirs(save_dir, exist_ok=True)
        save_path = os.path.join(save_dir, file_info["name"])
        received_md5 = hashlib.md5(file_info["data"]).hexdigest()
        if received_md5 != file_info["md5"]:
            self._display_message("系统", f"文件校验失败: {file_info['name']}", is_error=True)
            return
        with open(save_path, "wb") as f:
            f.write(file_info["data"])
        self._display_message("系统", f"文件保存成功: {save_path}  (MD5: {received_md5})")
        file_info = {
            "name": file_info["name"],
            "size": file_info["size"],
            "saved_path": save_path
        }
        self._save_chat_log("系统", f"文件接收完成: {file_info['name']}", msg_type="file", file_info=file_info)

    def playsound(self):
        if self.settings.get("enable_sound", True):
            try:
                winsound.PlaySound(r"C:\Windows\Media\Windows Notify Messaging.wav", winsound.SND_FILENAME)
            except:
                pass

    def run(self):
        self.window.mainloop()
        try:
            self.client_socket.close()
        except:
            pass

    def on_closing(self):
        if messagebox.askokcancel("退出", "确定要退出吗？"):
            self.window.destroy()

    def download_file_from_server(self, file_name):
        try:
            # 文件名合法性校验
            if re.search(r"[\\/]|\.\.", file_name):
                messagebox.showerror("非法文件名", "文件名不能包含..、/、\\字符")
                return
            file_port = int(self.settings.get("file_port", 12346))
            file_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            file_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
            file_socket.connect((self.settings["server_ip"], file_port))
            req = f"GET:{file_name}\n".encode("utf-8")
            file_socket.sendall(req)
            header_buffer = b""
            while True:
                chunk = file_socket.recv(1024)
                if not chunk:
                    if b"\n" not in header_buffer:
                        messagebox.showerror("下载失败", "未收到文件头")
                        file_socket.close()
                        return
                    else:
                        break
                header_buffer += chunk
                if b"\n" in header_buffer:
                    break
            if b"\n" not in header_buffer:
                messagebox.showerror("下载失败", "未收到完整文件头")
                file_socket.close()
                return
            header_line, remain = header_buffer.split(b"\n", 1)
            if header_line.startswith(b"ERROR:FileNotFound"):
                messagebox.showerror("下载失败", "服务器未找到该文件")
                file_socket.close()
                return
            header = header_line.decode('utf-8')
            if not header.startswith("FILE_INIT:"):
                messagebox.showerror("下载失败", f"无效文件头: {header}")
                file_socket.close()
                return
            _, file_name, file_size, md5_hex = header.split(":", 3)
            file_name = file_name.strip()
            file_size = int(file_size)
            md5_hex = md5_hex.strip()
            received = len(remain)
            file_data = bytearray(remain)

            # 创建下载进度窗口
            progress_win = Toplevel(self.window)
            progress_win.title(f"下载任务: {file_name}")
            progress_win.geometry("400x150")
            progress_win.resizable(False, False)
            progress_win.transient(self.window)
            progress_win.grab_set()
            progress_win.attributes('-topmost', True)
            # 居中
            self.window.update_idletasks()
            x = self.window.winfo_x() + (self.window.winfo_width() - 400) // 2
            y = self.window.winfo_y() + (self.window.winfo_height() - 150) // 2
            progress_win.geometry(f"400x150+{x}+{y}")
            ttk.Label(progress_win, text=f"{file_name}").pack(pady=10)
            progress_var = tk.DoubleVar(value=0)
            progress_bar = ttk.Progressbar(progress_win, maximum=100, length=350, variable=progress_var)
            progress_bar.pack(pady=5)
            progress_label = ttk.Label(progress_win, text="0.0% (0KB/{:.1f}KB)".format(file_size / 1024))
            progress_label.pack(pady=5)
            btn_frame = ttk.Frame(progress_win)
            btn_frame.pack(pady=5)
            open_btn = ttk.Button(btn_frame, text="打开文件所在目录", state="disabled")
            open_btn.pack(side="left", padx=10)
            close_btn = ttk.Button(btn_frame, text="关闭", command=progress_win.destroy)
            close_btn.pack(side="left", padx=10)

            def open_folder():
                folder = os.path.abspath(os.path.join("config", "received_files"))
                file_abspath = os.path.abspath(save_path)
                if sys.platform.startswith('win'):
                    subprocess.Popen(f'explorer /select,"{file_abspath}"')
                elif sys.platform.startswith('darwin'):
                    subprocess.Popen(['open', '-R', file_abspath])
                else:
                    subprocess.Popen(['xdg-open', folder])
                progress_win.destroy()

            open_btn.config(command=open_folder)

            save_dir = os.path.join("config", "received_files")
            os.makedirs(save_dir, exist_ok=True)
            save_path = os.path.join(save_dir, file_name)
            if os.path.exists(save_path):
                name, ext = os.path.splitext(file_name)
                timestamp = time.strftime("%Y%m%d_%H%M%S")
                save_path = os.path.join(save_dir, f"{name}_{timestamp}{ext}")

            # 下载循环，实时更新进度
            while received < file_size:
                chunk = file_socket.recv(min(4096, file_size - received))
                if not chunk:
                    messagebox.showerror("下载失败", "文件接收中断")
                    file_socket.close()
                    progress_win.destroy()
                    return
                file_data.extend(chunk)
                received += len(chunk)
                percent = received / file_size * 100
                progress_var.set(percent)
                progress_label.config(
                    text="{:.1f}% ({:.1f}KB/{:.1f}KB)".format(percent, received / 1024, file_size / 1024))
                progress_win.update()
            file_socket.close()
            received_md5 = hashlib.md5(file_data).hexdigest()
            if received_md5 != md5_hex:
                messagebox.showerror("下载失败", f"文件校验失败\n预期:{md5_hex}\n实际:{received_md5}")
                progress_win.destroy()
                return
            with open(save_path, "wb") as f:
                f.write(file_data)
            self._display_message("系统", f"下载成功，文件已保存至: {save_path}")
            # 下载完成后更新进度窗口
            progress_var.set(100)
            progress_label.config(text="下载完成！")
            open_btn.config(state="normal")
            progress_win.title(f"下载完成: {file_name}")
        except Exception as e:
            messagebox.showerror("下载失败", str(e))

    @staticmethod
    def _load_settings() -> dict:
        try:
            with open(os.path.join("config", "client_settings.json"), "r", encoding="utf-8") as f:
                settings = json.load(f)
                settings.setdefault("nickname", "")
                settings.setdefault("server_ip", "")
                settings.setdefault("server_port", 12345)
                settings.setdefault("file_port", 12346)
                return settings
        except (FileNotFoundError, json.JSONDecodeError):
            return {"server_ip": "127.0.0.1", "server_port": 12345, "file_port": 12346, "nickname": ""}


if __name__ == "__main__":
    # 修改主入口逻辑，先显示登录窗口
    login_window = LoginWindow()
    if login_window.nickname:
        client = ChatClient(login_window.nickname)  # 将用户名传递给主窗口
        client.run()
